home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Cream of the Crop 20
/
Cream of the Crop 20 (Terry Blount) (1996).iso
/
faq
/
cppnl010.zip
/
CPPNL010.TXT
next >
Wrap
Text File
|
1996-05-07
|
17KB
|
518 lines
Issue #010
May, 1996
Contents:
Introduction to Templates Part 2 - Class Templates
Introduction to Stream I/O Part 5 - Streambuf
Using C++ as a Better C Part 10 - General Initializers
Performance - Per-class New/Delete
INTRODUCTION TO TEMPLATES PART 2 - CLASS TEMPLATES
To continue our introduction of C++ templates, we'll be saying a few
things about class templates in this issue. Templates are a part of
the language still undergoing major changes, and it's tricky to figure
out just what to say. But we'll cover some basics that are well
accepted and in current usage.
A skeleton for a class template, and definitions of a member function
and a static data item, looks like this:
template <class T> class A {
void f();
static T x;
};
template <class T> void A<T>::f()
{
// stuff
}
template <class T> T A<T>::x = 0;
T is a placeholder for a template type argument, and is bound to that
argument when the template is instantiated. For example, if I say:
A<double> a;
then the type value of T is "double". The binding of template
arguments and the generation of an actual class from a template is a
process known as "instantiation". You can view a template as a
skeleton or macro or framework. When specific types, such as double,
are added to this skeleton, the result is an actual C++ class.
Template arguments may also be constant expressions:
template <int N> struct A {
// stuff
};
A<-37> a;
This feature is useful in the case where you want to pass a size into
the template. For example, a Vector template might accept a type
argument that tells what type of elements will be operated on, and a
size argument giving the vector length:
template <class T, int N> class Vector {
// stuff
};
Vector<float, 100> v;
A template argument may have a default specified (this feature is not
widely available as yet):
template <class T = int, int N = 100> class Vector {
// stuff
};
Vector<float, 50> v1; // Vector<float, 50>
Vector<char> v2; // Vector<char, 100>
Vector<> v3; // Vector<int, 100>
To see how some of these basic ideas fit together, let's actually
build a simple Vector template, with set() and get() functions:
template <class T, int N = 100> class Vector {
T vec[N];
public:
void set(int pos, T val);
T get(int pos);
};
template <class T, int N> void Vector<T, N>::set(int pos, T val)
{
if (pos < 0 || pos >= N)
; // give error of some kind
vec[pos] = val;
}
template <class T, int N> T Vector<T, N>::get(int pos)
{
if (pos < 0 || pos >= N)
; // give error of some kind
return vec[pos];
}
// driver program
int main()
{
Vector<double, 10> v;
int i = 0;
double d = 0.0;
// set locations in vector
for (i = 0; i < 10; i++)
v.set(i, double(i * i));
// get location values from vector
for (i = 0; i < 10; i++)
d = v.get(i);
return 0;
}
Actual values are stored in a private vector of type T and length N.
In a real Vector class we might overload operator[] to provide a
natural sort of interface such as an actual vector has.
What would happen if we said something like:
Vector<char, -1000> v;
This is an example of code that is legal until the template is
actually instantiated into a class. Because a member like:
char vec[-1000];
is not valid (you can't have arrays of negative or zero size), this
usage will be flagged as an error when instantiation is done.
The process of instantiation itself is a bit tricky. If I have 10
translation units (source files), and each uses an instantiated class:
Vector<unsigned long, 250>
where does the code for the instantiated class's member functions go?
The template definition itself resides most commonly in a header file,
so that it can be accessed everywhere and because template code has
some different properties than other source code.
This is an extremely hard problem for a compiler to solve. One
solution is to make all template functions inline and duplicate the
code for them per translation unit. This results in very fast but
potentially bulky code.
Another approach, which works if you have control over the object file
format and the linker, is to generate duplicate instantiations per
object file and then use the linker to merge them.
Yet another approach is to create auxiliary files or directories
("repositories") that have a memory of what has been instantiated in
which object file, and use that state file in conjunction with the
compiler and linker to control the instantiation process.
There are also schemes for explicitly forcing instantiation to take
place. We'll discuss these in a future issue. The instantiation
issue is usually hidden from a programmer, but sometimes becomes
visible, for example if the programmer notices that object file sizes
seem bloated.
INTRODUCTION TO STREAM I/O PART 5 - STREAMBUF
In previous issues we talked about various ways of copying files using
stream I/O, some of the ways of affecting I/O operations by specifying
unit buffering or not and tying one stream to another, and so on.
Another way of copying input to output using stream I/O is to say:
#include <iostream.h>
int main()
{
int c;
while ((c = cin.rdbuf()->sbumpc()) != EOF)
cout.rdbuf()->sputc(c);
return 0;
}
This scheme uses what are known as streambufs, underlying buffers used
in the stream I/O package. An expression:
cin.rdbuf()->sbumpc()
says "obtain the streambuf pointer for the standard input stream
(cin) and grab the next character from it and then advance the
internal pointer within the buffer". Similarly,
cout.rdbuf()->sputc(c)
adds a character to the output buffer.
Doing I/O in this way is lower-level than some other approaches, but
correspondingly faster. If we summarize the four file-copying methods
we've studied (see issues #008 and #009 for code examples of them),
from slowest to fastest, they might be as follows.
Copy a character at a time with >> and <<:
cin.tie(0);
cin.unsetf(ios::skipws);
while (cin >> c)
cout << c;
Copy using get() and put():
ifstream ifs(argv[1], ios::in | ios::binary);
ofstream ofs(argv[2], ios::out | ios::binary);
while (ifs.get(c))
ofs.put(c);
Copy with streambufs (above):
while ((c = cin.rdbuf()->sbumpc()) != EOF)
cout.rdbuf()->sputc(c);
Copy with streambufs but explicit copying buried:
ifstream ifs(argv[1], ios::in | ios::binary);
ofstream ofs(argv[2], ios::out | ios::binary);
ofs << ifs.rdbuf();
A table of relative times, for one popular C++ compiler, comes out
like so:
>>, << 100
get/put 72
streambuf 62
streambuf hidden 43
Actual times will vary for a given library. Perhaps the most critical
factor is whether functions that are used in a given case are inlined
or not. Note also that if you are copying binary files you need to be
careful with the way copying is done.
Why the time differences? All of these methods use streambufs in some
form. But the slowest method, using >> and <<, also does additional
pro